/*
 * %W% %E%
 *
 * Copyright (c) 2006, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

package com.sun.security.auth.module;

import javax.security.auth.x500.X500Principal;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AuthProvider;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.UnrecoverableKeyException;
import java.security.cert.*;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.ResourceBundle;
import javax.security.auth.Destroyable;
import javax.security.auth.DestroyFailedException;
import javax.security.auth.Subject;
import javax.security.auth.x500.*;
import javax.security.auth.Subject;
import javax.security.auth.x500.*;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.ConfirmationCallback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.TextOutputCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;

import sun.security.util.AuthResources;
import sun.security.util.Password;

/**
 * Provides a JAAS login module that prompts for a key store alias and
 * populates the subject with the alias's principal and credentials. Stores
 * an <code>X500Principal</code> for the subject distinguished name of the 
 * first certificate in the alias's credentials in the subject's principals,
 * the alias's certificate path in the subject's public credentials, and a
 * <code>X500PrivateCredential</code> whose certificate is the first
 * certificate in the alias's certificate path and whose private key is the
 * alias's private key in the subject's private credentials. <p>
 *
 * Recognizes the following options in the configuration file:
 * <dl>
 *
 * <dt> <code>keyStoreURL</code> </dt>
 * <dd> A URL that specifies the location of the key store.  Defaults to
 *	a URL pointing to the .keystore file in the directory specified by the
 *	<code>user.home</code> system property.  The input stream from this
 *	URL is passed to the <code>KeyStore.load</code> method.
 *	"NONE" may be specified if a <code>null</code> stream must be
 *	passed to the <code>KeyStore.load</code> method.
 *	"NONE" should be specified if the KeyStore resides
 *	on a hardware token device, for example.</dd>
 *
 * <dt> <code>keyStoreType</code> </dt>
 * <dd> The key store type.  If not specified, defaults to the result of
 *      calling <code>KeyStore.getDefaultType()</code>.
 *	If the type is "PKCS11", then keyStoreURL must be "NONE"
 *	and privateKeyPasswordURL must not be specified.</dd>
 *
 * <dt> <code>keyStoreProvider</code> </dt>
 * <dd> The key store provider.  If not specified, uses the standard search
 *      order to find the provider. </dd>
 *
 * <dt> <code>keyStoreAlias</code> </dt>
 * <dd> The alias in the key store to login as.  Required when no callback
 *	handler is provided.  No default value. </dd>
 *
 * <dt> <code>keyStorePasswordURL</code> </dt>
 * <dd> A URL that specifies the location of the key store password.  Required
 *	when no callback handler is provided and
 *	<code>protected</code> is false.
 *	No default value. </dd>
 *
 * <dt> <code>privateKeyPasswordURL</code> </dt>
 * <dd> A URL that specifies the location of the specific private key password
 *	needed to access the private key for this alias.  
 *      The keystore password
 *	is used if this value is needed and not specified. </dd>
 *
 * <dt> <code>protected</code> </dt>
 * <dd> This value should be set to "true" if the KeyStore
 *	has a separate, protected authentication path
 *	(for example, a dedicated PIN-pad attached to a smart card).
 *	Defaults to "false". If "true" keyStorePasswordURL and
 *	privateKeyPasswordURL must not be specified.</dd>
 *
 * </dl>
 */
public class KeyStoreLoginModule implements LoginModule {

   static final java.util.ResourceBundle rb =
        java.util.ResourceBundle.getBundle("sun.security.util.AuthResources");

    /* -- Fields -- */

    private static final int UNINITIALIZED = 0;
    private static final int INITIALIZED = 1;
    private static final int AUTHENTICATED = 2;
    private static final int LOGGED_IN = 3;

    private static final int PROTECTED_PATH = 0;
    private static final int TOKEN = 1;
    private static final int NORMAL = 2;

    private static final String NONE = "NONE";
    private static final String P11KEYSTORE = "PKCS11";

    private static final TextOutputCallback bannerCallback =
		new TextOutputCallback
			(TextOutputCallback.INFORMATION,
			rb.getString("Please enter keystore information"));
    private final ConfirmationCallback confirmationCallback =
		new ConfirmationCallback
			(ConfirmationCallback.INFORMATION,
			ConfirmationCallback.OK_CANCEL_OPTION,
			ConfirmationCallback.OK);

    private Subject subject;
    private CallbackHandler callbackHandler;
    private Map sharedState;
    private Map options;

    private char[] keyStorePassword;
    private char[] privateKeyPassword;
    private KeyStore keyStore;

    private String keyStoreURL;
    private String keyStoreType;
    private String keyStoreProvider;
    private String keyStoreAlias;
    private String keyStorePasswordURL;
    private String privateKeyPasswordURL;
    private boolean debug;
    private javax.security.auth.x500.X500Principal principal;
    private Certificate[] fromKeyStore;
    private java.security.cert.CertPath certP = null;
    private X500PrivateCredential privateCredential;
    private int status = UNINITIALIZED;
    private boolean nullStream = false; 
    private boolean token = false; 
    private boolean protectedPath = false; 

    /* -- Methods -- */
  
    /**
     * Initialize this <code>LoginModule</code>.
     *
     * <p>
     *
     * @param subject the <code>Subject</code> to be authenticated. <p>
     *
     * @param callbackHandler a <code>CallbackHandler</code> for communicating
     *			with the end user (prompting for usernames and
     *			passwords, for example),
     *			which may be <code>null</code>. <p>
     *
     * @param sharedState shared <code>LoginModule</code> state. <p>
     *
     * @param options options specified in the login
     *			<code>Configuration</code> for this particular
     *			<code>LoginModule</code>.
     */

    public void initialize(Subject subject,
			   CallbackHandler callbackHandler,
			   Map<String,?> sharedState,
			   Map<String,?> options)
    {
 	this.subject = subject;
	this.callbackHandler = callbackHandler;
	this.sharedState = sharedState;
	this.options = options;

	processOptions();
	status = INITIALIZED;
    }

    private void processOptions() {
	keyStoreURL = (String) options.get("keyStoreURL");
	if (keyStoreURL == null) {
	    keyStoreURL =
		"file:" +
		System.getProperty("user.home").replace(
		    File.separatorChar, '/') +
		'/' + ".keystore";
	} else if (NONE.equals(keyStoreURL)) {
	    nullStream = true;
	}
	keyStoreType = (String) options.get("keyStoreType");
	if (keyStoreType == null) {
	    keyStoreType = KeyStore.getDefaultType();
	}
	if (P11KEYSTORE.equalsIgnoreCase(keyStoreType)) {
	    token = true;
	}

	keyStoreProvider = (String) options.get("keyStoreProvider");

	keyStoreAlias = (String) options.get("keyStoreAlias");

	keyStorePasswordURL = (String) options.get("keyStorePasswordURL");

	privateKeyPasswordURL = (String) options.get("privateKeyPasswordURL");

	protectedPath = "true".equalsIgnoreCase((String)options.get
					("protected"));

	debug = "true".equalsIgnoreCase((String) options.get("debug"));
	if (debug) {
	    debugPrint(null);
	    debugPrint("keyStoreURL=" + keyStoreURL);
	    debugPrint("keyStoreType=" + keyStoreType);
	    debugPrint("keyStoreProvider=" + keyStoreProvider);
	    debugPrint("keyStoreAlias=" + keyStoreAlias);
	    debugPrint("keyStorePasswordURL=" + keyStorePasswordURL);
	    debugPrint("privateKeyPasswordURL=" + privateKeyPasswordURL);
	    debugPrint("protectedPath=" + protectedPath);
	    debugPrint(null);
	}
    }
    
    /**
     * Authenticate the user.
     *
     * <p> Get the Keystore alias and relevant passwords.
     * Retrieve the alias's principal and credentials from the Keystore.
     *
     * <p>
     *
     * @exception FailedLoginException if the authentication fails. <p>
     *
     * @return true in all cases (this <code>LoginModule</code>
     *		should not be ignored).
     */

    public boolean login() throws LoginException {
	switch (status) {
	case UNINITIALIZED:
	default:
	    throw new LoginException("The login module is not initialized");
	case INITIALIZED:
	case AUTHENTICATED:

	    if (token && !nullStream) {
		throw new LoginException
			("if keyStoreType is " + P11KEYSTORE +
			" then keyStoreURL must be " + NONE);
	    }

	    if (token && privateKeyPasswordURL != null) {
		throw new LoginException
			("if keyStoreType is " + P11KEYSTORE +
			" then privateKeyPasswordURL must not be specified");
	    }

	    if (protectedPath &&
		(keyStorePasswordURL != null ||
			privateKeyPasswordURL != null)) {
		throw new LoginException
			("if protected is true then keyStorePasswordURL and " +
			"privateKeyPasswordURL must not be specified");
	    }

	    // get relevant alias and password info

	    if (protectedPath) {
		getAliasAndPasswords(PROTECTED_PATH);
	    } else if (token) {
		getAliasAndPasswords(TOKEN);
	    } else {
		getAliasAndPasswords(NORMAL);
	    }

	    // log into KeyStore to retrieve data,
	    // then clear passwords

	    try {
		getKeyStoreInfo();
	    } finally {
		if (privateKeyPassword != null &&
		    privateKeyPassword != keyStorePassword) {
		    Arrays.fill(privateKeyPassword, '\0');
		    privateKeyPassword = null;
		}
		if (keyStorePassword != null) {
		    Arrays.fill(keyStorePassword, '\0');
		    keyStorePassword = null;
		}
	    }
	    status = AUTHENTICATED;
	    return true;
	case LOGGED_IN:
	    return true;
	}
    }

    /** Get the alias and passwords to use for looking up in the KeyStore. */
    private void getAliasAndPasswords(int env) throws LoginException {
	if (callbackHandler == null) {

	    // No callback handler - check for alias and password options

	    switch (env) {
	    case PROTECTED_PATH:
		checkAlias();
		break;
	    case TOKEN:
		checkAlias();
		checkStorePass();
		break;
	    case NORMAL:
		checkAlias();
		checkStorePass();
		checkKeyPass();
		break;
	    }

	} else {

	    // Callback handler available - prompt for alias and passwords

	    NameCallback aliasCallback;
	    if (keyStoreAlias == null || keyStoreAlias.length() == 0) {
		aliasCallback = new NameCallback(
				        rb.getString("Keystore alias: "));
	    } else {
		aliasCallback =
		    new NameCallback(rb.getString("Keystore alias: "), 
				     keyStoreAlias);
	    }

	    PasswordCallback storePassCallback = null;
	    PasswordCallback keyPassCallback = null;

	    switch (env) {
	    case PROTECTED_PATH:
		break;
	    case NORMAL:
		keyPassCallback = new PasswordCallback
		    (rb.getString("Private key password (optional): "), false);
		// fall thru
	    case TOKEN:
		storePassCallback = new PasswordCallback
		    (rb.getString("Keystore password: "), false);
		break;
	    }
	    prompt(aliasCallback, storePassCallback, keyPassCallback);
	}

	if (debug) {
	    debugPrint("alias=" + keyStoreAlias);
	}
    }

    private void checkAlias() throws LoginException {
	if (keyStoreAlias == null) {
	    throw new LoginException
		("Need to specify an alias option to use " +
		"KeyStoreLoginModule non-interactively.");
	}
    }

    private void checkStorePass() throws LoginException {
	if (keyStorePasswordURL == null) {
	    throw new LoginException
		("Need to specify keyStorePasswordURL option to use " +
		"KeyStoreLoginModule non-interactively.");
	}
	try {
	    InputStream in = new URL(keyStorePasswordURL).openStream();
	    keyStorePassword = Password.readPassword(in);
	    in.close();
	} catch (IOException e) {
	    LoginException le = new LoginException
		("Problem accessing keystore password \"" +
		keyStorePasswordURL + "\"");
	    le.initCause(e);
	    throw le;
	}
    }

    private void checkKeyPass() throws LoginException {
	if (privateKeyPasswordURL == null) {
	    privateKeyPassword = keyStorePassword;
	} else {
	    try {
		InputStream in = new URL(privateKeyPasswordURL).openStream();
		privateKeyPassword = Password.readPassword(in);
		in.close();
	    } catch (IOException e) {
		LoginException le = new LoginException
			("Problem accessing private key password \"" +
			privateKeyPasswordURL + "\"");
		le.initCause(e);
		throw le;
	    }
	}
    }

    private void prompt(NameCallback aliasCallback,
			PasswordCallback storePassCallback,
			PasswordCallback keyPassCallback)
		throws LoginException {

	if (storePassCallback == null) {

	    // only prompt for alias

	    try {
		callbackHandler.handle(
		    new Callback[] {
			bannerCallback, aliasCallback, confirmationCallback
		    });
	    } catch (IOException e) {
		LoginException le = new LoginException
			("Problem retrieving keystore alias");
		le.initCause(e);
		throw le;
	    } catch (UnsupportedCallbackException e) {
		throw new LoginException(
		    "Error: " + e.getCallback().toString() +
		    " is not available to retrieve authentication " +
		    " information from the user");
	    }

	    int confirmationResult = confirmationCallback.getSelectedIndex();

	    if (confirmationResult == ConfirmationCallback.CANCEL) {
		throw new LoginException("Login cancelled");
	    }

	    saveAlias(aliasCallback);

	} else if (keyPassCallback == null) {

	    // prompt for alias and key store password

	    try {
		callbackHandler.handle(
		    new Callback[] {
			bannerCallback, aliasCallback,
			storePassCallback, confirmationCallback
		    });
	    } catch (IOException e) {
		LoginException le = new LoginException
			("Problem retrieving keystore alias and password");
		le.initCause(e);
		throw le;
	    } catch (UnsupportedCallbackException e) {
		throw new LoginException(
		    "Error: " + e.getCallback().toString() +
		    " is not available to retrieve authentication " +
		    " information from the user");
	    }

	    int confirmationResult = confirmationCallback.getSelectedIndex();

	    if (confirmationResult == ConfirmationCallback.CANCEL) {
		throw new LoginException("Login cancelled");
	    }

	    saveAlias(aliasCallback);
	    saveStorePass(storePassCallback);

	} else {

	    // prompt for alias, key store password, and key password

	    try {
		callbackHandler.handle(
		    new Callback[] {
			bannerCallback, aliasCallback,
			storePassCallback, keyPassCallback,
			confirmationCallback
		    });
	    } catch (IOException e) {
		LoginException le = new LoginException
			("Problem retrieving keystore alias and passwords");
		le.initCause(e);
		throw le;
	    } catch (UnsupportedCallbackException e) {
		throw new LoginException(
		    "Error: " + e.getCallback().toString() +
		    " is not available to retrieve authentication " +
		    " information from the user");
	    }

	    int confirmationResult = confirmationCallback.getSelectedIndex();

	    if (confirmationResult == ConfirmationCallback.CANCEL) {
		throw new LoginException("Login cancelled");
	    }

	    saveAlias(aliasCallback);
	    saveStorePass(storePassCallback);
	    saveKeyPass(keyPassCallback);
	}
    }

    private void saveAlias(NameCallback cb) {
	keyStoreAlias = cb.getName();
    }
   
    private void saveStorePass(PasswordCallback c) {
	keyStorePassword = c.getPassword();
	if (keyStorePassword == null) {
	    /* Treat a NULL password as an empty password */
	    keyStorePassword = new char[0];
	}
	c.clearPassword();
    }

    private void saveKeyPass(PasswordCallback c) {
	privateKeyPassword = c.getPassword();
	if (privateKeyPassword == null || privateKeyPassword.length == 0) {
	    /*
	     * Use keystore password if no private key password is
	     * specified.
	     */
	    privateKeyPassword = keyStorePassword;
	}
	c.clearPassword();
    }

    /** Get the credentials from the KeyStore. */
    private void getKeyStoreInfo() throws LoginException {

	/* Get KeyStore instance */
	try {
	    if (keyStoreProvider == null) {
		keyStore = KeyStore.getInstance(keyStoreType);
	    } else {
		keyStore =
		    KeyStore.getInstance(keyStoreType, keyStoreProvider);
	    }
	} catch (KeyStoreException e) {
	    LoginException le = new LoginException
		("The specified keystore type was not available");
	    le.initCause(e);
	    throw le;
	} catch (NoSuchProviderException e) {
	    LoginException le = new LoginException
		("The specified keystore provider was not available");
	    le.initCause(e);
	    throw le;
	}

	/* Load KeyStore contents from file */
	try {
	    if (nullStream) {
		// if using protected auth path, keyStorePassword will be null
		keyStore.load(null, keyStorePassword);
	    } else {
		InputStream in = new URL(keyStoreURL).openStream();
		keyStore.load(in, keyStorePassword);
		in.close();
	    }
	} catch (MalformedURLException e) {
	    LoginException le = new LoginException
				("Incorrect keyStoreURL option");
	    le.initCause(e);
	    throw le;
	} catch (GeneralSecurityException e) {
	    LoginException le = new LoginException
				("Error initializing keystore");
	    le.initCause(e);
	    throw le;
	} catch (IOException e) {
	    LoginException le = new LoginException
				("Error initializing keystore");
	    le.initCause(e);
	    throw le;
	}

	/* Get certificate chain and create a certificate path */
	try {
	    fromKeyStore =
		keyStore.getCertificateChain(keyStoreAlias);
	    if (fromKeyStore == null
		|| fromKeyStore.length == 0
		|| !(fromKeyStore[0] instanceof X509Certificate))
	    {
		throw new FailedLoginException(
		    "Unable to find X.509 certificate chain in keystore");
	    } else {
		LinkedList certList = new LinkedList();
		for (int i=0; i < fromKeyStore.length; i++) {
		    certList.add(fromKeyStore[i]);
		}
		CertificateFactory certF= 
		    CertificateFactory.getInstance("X.509");
		certP = 
		    certF.generateCertPath(certList);	
	    }
	} catch (KeyStoreException e) {
	    LoginException le = new LoginException("Error using keystore");
	    le.initCause(e);
	    throw le;
	} catch (CertificateException ce) {
	    LoginException le = new LoginException
		("Error: X.509 Certificate type unavailable");
	    le.initCause(ce);
	    throw le;
	}

	/* Get principal and keys */
	try {
	    X509Certificate certificate = (X509Certificate)fromKeyStore[0];
	    principal = new javax.security.auth.x500.X500Principal
		(certificate.getSubjectDN().getName());

	    // if token, privateKeyPassword will be null
	    Key privateKey = keyStore.getKey(keyStoreAlias, privateKeyPassword);
	    if (privateKey == null
		|| !(privateKey instanceof PrivateKey))
	    {
		throw new FailedLoginException(
		    "Unable to recover key from keystore");
	    }

	    privateCredential = new X500PrivateCredential(
		certificate, (PrivateKey) privateKey, keyStoreAlias);
	} catch (KeyStoreException e) {
	    LoginException le = new LoginException("Error using keystore");
	    le.initCause(e);
	    throw le;
	} catch (NoSuchAlgorithmException e) {
	    LoginException le = new LoginException("Error using keystore");
	    le.initCause(e);
	    throw le;
	} catch (UnrecoverableKeyException e) {
	    FailedLoginException fle = new FailedLoginException
				("Unable to recover key from keystore");
	    fle.initCause(e);
	    throw fle;
	}
	if (debug) {
	    debugPrint("principal=" + principal +
		       "\n certificate="
		       + privateCredential.getCertificate() +
		       "\n alias =" + privateCredential.getAlias());
	}
    }

    /**
     * Abstract method to commit the authentication process (phase 2).
     *
     * <p> This method is called if the LoginContext's
     * overall authentication succeeded
     * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
     * succeeded).
     *
     * <p> If this LoginModule's own authentication attempt
     * succeeded (checked by retrieving the private state saved by the
     * <code>login</code> method), then this method associates a
     * <code>X500Principal</code> for the subject distinguished name of the 
     * first certificate in the alias's credentials in the subject's
     * principals,the alias's certificate path in the subject's public 
     * credentials, and a<code>X500PrivateCredential</code> whose certificate
     * is the first  certificate in the alias's certificate path and whose
     * private key is the alias's private key in the subject's private
     * credentials.  If this LoginModule's own
     * authentication attempted failed, then this method removes
     * any state that was originally saved.
     *
     * <p>
     *
     * @exception LoginException if the commit fails
     *
     * @return true if this LoginModule's own login and commit
     *		attempts succeeded, or false otherwise.
     */

    public boolean commit() throws LoginException {
	switch (status) {
	case UNINITIALIZED:
	default:
	    throw new LoginException("The login module is not initialized");
	case INITIALIZED:
	    logoutInternal();
	    throw new LoginException("Authentication failed");
	case AUTHENTICATED:
	    if (commitInternal()) {
		return true;
	    } else {
		logoutInternal();
		throw new LoginException("Unable to retrieve certificates");
	    }
	case LOGGED_IN:
	    return true;
	}
    }

    private boolean commitInternal() throws LoginException {
	/* If the subject is not readonly add to the principal and credentials
	 * set; otherwise just return true
	 */
	if (subject.isReadOnly()) {
	    throw new LoginException ("Subject is set readonly");
	} else {
	    subject.getPrincipals().add(principal);
	    subject.getPublicCredentials().add(certP);
	    subject.getPrivateCredentials().add(privateCredential);
	    status = LOGGED_IN;
	    return true;
	}
    }

    /**
     * <p> This method is called if the LoginContext's
     * overall authentication failed.
     * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
     * did not succeed).
     *
     * <p> If this LoginModule's own authentication attempt
     * succeeded (checked by retrieving the private state saved by the
     * <code>login</code> and <code>commit</code> methods),
     * then this method cleans up any state that was originally saved.
     *
     * <p> If the loaded KeyStore's provider extends
     * <code>java.security.AuthProvider</code>,
     * then the provider's <code>logout</code> method is invoked.
     *
     * <p>
     *
     * @exception LoginException if the abort fails.
     *
     * @return false if this LoginModule's own login and/or commit attempts
     *		failed, and true otherwise.
     */
	
    public boolean abort() throws LoginException {
	switch (status) {
	case UNINITIALIZED:
	default:
	    return false;
	case INITIALIZED:
	    return false;
	case AUTHENTICATED:
	    logoutInternal();
	    return true;
	case LOGGED_IN:
	    logoutInternal();
	    return true;
	}
    }
    /**
     * Logout a user.
     *
     * <p> This method removes the Principals, public credentials and the
     * private credentials that were added by the <code>commit</code> method.
     *
     * <p> If the loaded KeyStore's provider extends
     * <code>java.security.AuthProvider</code>,
     * then the provider's <code>logout</code> method is invoked.
     *
     * <p>
     *
     * @exception LoginException if the logout fails.
     *
     * @return true in all cases since this <code>LoginModule</code>
     *		should not be ignored.
     */

    public boolean logout() throws LoginException {
	if (debug)
	    debugPrint("Entering logout " + status);
	switch (status) {
	case UNINITIALIZED:
	    throw new LoginException
		("The login module is not initialized");
	case INITIALIZED:
	case AUTHENTICATED:
	default:
	   // impossible for LoginModule to be in AUTHENTICATED 
	   // state
	   // assert status != AUTHENTICATED;
	    return false;
	case LOGGED_IN:
	    logoutInternal();
	    return true;
	}
    }

    private void logoutInternal() throws LoginException {
	if (debug) {
	    debugPrint("Entering logoutInternal");
	}

	// assumption is that KeyStore.load did a login -
	// perform explicit logout if possible
	LoginException logoutException = null;
	Provider provider = keyStore.getProvider();
	if (provider instanceof AuthProvider) {
	    AuthProvider ap = (AuthProvider)provider;
	    try {
		ap.logout();
		if (debug) {
		    debugPrint("logged out of KeyStore AuthProvider");
		}
	    } catch (LoginException le) {
		// save but continue below
		logoutException = le;
	    }
	}

	if (subject.isReadOnly()) {
	    // attempt to destroy the private credential
	    // even if the Subject is read-only
	    principal = null;
	    certP = null;
	    status = INITIALIZED;
	    // destroy the private credential
	    Iterator it = subject.getPrivateCredentials().iterator();
	    while (it.hasNext()) {
		Object obj = it.next();
		if (privateCredential.equals(obj)) {
		    privateCredential = null;
		    try {
			((Destroyable)obj).destroy();
			if (debug)
			    debugPrint("Destroyed private credential, " +
				       obj.getClass().getName());
			break;
		    } catch (DestroyFailedException dfe) {
			LoginException le = new LoginException
			    ("Unable to destroy private credential, " 
			     + obj.getClass().getName());
			le.initCause(dfe);
			throw le;
		    }
		}
	    }
	    
	    // throw an exception because we can not remove
	    // the principal and public credential from this
	    // read-only Subject
	    throw new LoginException
		("Unable to remove Principal (" 
		 + "X500Principal "
		 + ") and public credential (certificatepath) "
		 + "from read-only Subject");
	}
	if (principal != null) {
	    subject.getPrincipals().remove(principal);
	    principal = null;
	}
	if (certP != null) {
	    subject.getPublicCredentials().remove(certP);
	    certP = null;
	}
	if (privateCredential != null) {
	    subject.getPrivateCredentials().remove(privateCredential);
	    privateCredential = null;
	}

	// throw pending logout exception if there is one
	if (logoutException != null) {
	    throw logoutException;
	}
	status = INITIALIZED;
    }

    private void debugPrint(String message) {
	// we should switch to logging API
	if (message == null) {
	    System.err.println();
	} else {
	    System.err.println("Debug KeyStoreLoginModule: " + message);
	}
    }
}
